5.3 select的底层实现及工作原理
select
语句是Go
语言中用于处理多个通道操作的一个强大工具,它能够在多个通道上同时进行非阻塞的选择操作。这对于实现并发程序的灵活性和复杂性处理非常有帮助。
本节我们将详细探讨select
的内部实现及工作原理。
本节代码存放目录为 lesson15
select的底层实现
select的基本结构
select
语句的实现涉及到以下几个核心部分:
通道的
case
列表:select
语句中每个case
会对应一个通道操作,编译器会将这些case
打包成一个select
操作列表。case
结构如下所示:type scase struct { c *hchan // chan elem unsafe.Pointer // data element }
随机化:为了避免
select
语句的饥饿问题(总是先处理某个case
),Go
语言的实现会对case
列表进行随机化处理。阻塞队列:如果所有的通道都无法立即进行操作,
select
语句会将当前的Goroutine
加入到每个通道的等待队列中,并阻塞Goroutine
,直到某个通道的操作可以进行。唤醒与继续:当某个通道的操作可以进行时,
select
会唤醒相关的Goroutine
,并继续执行与该通道关联的case
。
select的操作流程
我们可以以下步骤理解select
的操作流程:
初始化
select
的case
列表:编译器将每个case
操作(通道的接收或发送)打包到一个列表中。随机化
case
列表:为了避免饥饿,运行时会对这个case
列表进行随机打乱,使得每次select
的执行顺序都是随机的。遍历
case
列表:对于每个
case
,select
语句会检查对应通道是否可以立即进行操作。如果可以,则直接执行该
case
,并结束select
语句。如果不可以,则将当前
Goroutine
加入到该通道的等待队列中。
阻塞当前
Goroutine
:如果所有的通道都不能立即操作,
select
语句将阻塞当前的Goroutine
,直到其中一个通道可以进行操作。当某个通道准备好后,该
Goroutine
会被唤醒,执行与该通道关联的case
。
默认情况
default
:- 如果
select
语句中存在default
分支,并且所有通道都不能操作,那么select
会立即执行default
分支,而不会阻塞。
- 如果
我们可以通过下面的示意图来进行理解:
┌────────────────────────┐
│ select │
│ ┌───────────────────┐ │
│ │ case1: <- ch1 │ │
│ │ case2: <- ch2 │ │
│ │ case3: <- ch3 │ │
│ └───────────────────┘ │
└────────────────────────┘
│
▼
┌─────────────────────────┐
│ 运行时随机化 │
│ 随机打乱 case 列表 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 顺序检查 case │
│ 检查 case1、case2... │
│ 按随机后的顺序 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 执行一个可以操作的 case │
│ 例如:执行 case2 │
└─────────────────────────┘
│
▼
select 语句结束
select的实现原理
Go
语言中的select
语句依赖于调度器和通道的底层机制来实现。具体来说:
调度器:
select
语句会与Go
调度器紧密合作,当select
阻塞时,调度器会将当前Goroutine
挂起,并将其加入到通道的等待队列中。通道的队列:每个通道都有发送和接收的等待队列。当
select
中的某个通道准备好时,通道的机制会从队列中唤醒对应的Goroutine
。唤醒机制:当通道的状态发生变化时(例如一个通道的数据被接收或发送),通道会通过调度器唤醒阻塞在其上的
Goroutine
,然后继续执行select
语句的逻辑。
性能与使用建议
虽然select
非常强大,但是在使用时也有一些性能和设计方面的考虑:
避免滥用
select
:在高并发场景下,如果select
语句处理的通道数量过多,可能会带来一些性能开销。使用
default
分支:在某些情况下,添加default
分支可以防止select
语句永久阻塞,从而提高程序的响应性。关注
select
的随机性:由于select
语句的case
选择是随机化的,因此不要依赖某个固定的选择顺序,这样可以避免一些难以调试的问题。
下面代码演示了一个常用的使用案例:
func main() {
ch1 := make(chan int64, 2)
ch2 := make(chan int64, 2)
ch3 := make(chan int64, 2)
wg.Add(1)
go func() {
defer wg.Done()
for {
ch1 <- time.Now().Unix()
time.Sleep(time.Duration(1) * time.Second)
ch2 <- time.Now().Unix()
time.Sleep(time.Duration(1) * time.Second)
ch3 <- time.Now().Unix()
time.Sleep(time.Duration(1) * time.Second)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case t1 := <-ch1:
fmt.Println("Received from ch1, ", t1)
case t2 := <-ch2:
fmt.Println("Received from ch2, ", t2)
case t3 := <-ch3:
fmt.Println("Received from ch3, ", t3)
}
}
}()
wg.Wait()
}
小结
select
的主要作用就是用于对多个通道执行读取操作,这样一方面我们可以简化我们的程序,一方面我们也可以通过select
执行一些流程操作。
select
本质上就属于是监听了多个通道,所以我们不适合在select
中使用大批量的case
。